03 - 成本归因与选型
前置:01 篇的 token 指标;了解 Agent 网关 · 多租户与配额的虚拟密钥概念会更好。
本篇回答:把 token 数乘以单价为什么算不准、正确的数据模型长什么样、以及三个开源平台怎么选。
本篇会用到的词:
| 词 | 意思 |
|---|---|
| 虚拟密钥 | 网关自己签发的一套 key,key 上绑着团队、配额和可用模型范围。真实的 provider key 只有网关持有 —— 它是成本归因能成立的前提 |
| 打标(tagging) | 在请求上附加团队、项目、环境 这些维度,落库时一起存下来。事后加不了维度,因为历史数据里没有那个字段 |
| usage 明细 | provider 在响应里返回的 token 用量。注意它会区分「缓存命中的输入 token」和「新算的输入 token」,两者单价不同 |
| prompt caching | provider 提供的前缀缓存,命中的部分按折扣价计费。它是「只按总 token 数乘标准单价会系统性高估成本」的直接原因 |
| 分档单价 | 按 token 的种类(缓存命中输入 / 新输入 / 输出)分别定价,而不是一个统一价 |
| NOASSERTION | gh api 返回的许可证字段值,意思是「自动识别不出来」,不等于没有协议。看到它要自己去翻仓库里的 LICENSE |
一、成本归因的四个难点
1.1 调用者身份在链路中丢失
模型调用发生在最内层,"谁发起的"这一信息在最外层,中间隔着若干层编排:
身份未能一路透传时,得到的只有"某次调用花了 0.03 美元",无法归属。
结论:成本归因应做在网关层,而非应用层。 网关是唯一同时知道身份(来自虚拟密钥)与 token 数(来自响应)的位置。应用层实现意味着要在每层编排手工透传身份,漏一处即产生无法归属的开销。
1.2 一次用户请求对应 N 次计费调用
01 篇 3.2 节的 inference_calls 在这里成为关键:同一个问题,模型可能两步解决也可能绕十步,成本相差数倍。
因此成本看板的核心指标不是"平均每次调用的花费",而是平均每次用户请求的花费。两者的比值即平均步数,该值上升说明 Agent 在绕远路。
1.3 缓存改变了计费结构
Agent 网关 · 缓存描述的四层缓存对账单的影响各不相同:
| 层 | 对成本的影 响 | trace 可见性 |
|---|---|---|
| 网关精确匹配缓存 | 该次调用完全免费 | 需主动记录,否则该请求在 trace 中不存在 |
| 网关语义缓存 | 免费,但增加一次 embedding 调用成本 | 同上,且 embedding 那笔容易漏记 |
| Provider prompt caching | 前缀部分按折扣价计费 | 需读取 provider 返回的 usage 明细 |
| 引擎 prefix caching | 自建集群省算力,不影响外部账单 | 不影响 |
最易算错的是语义缓存:省下一次模型调用,但每次查询都增加一次 embedding 调用。embedding 成本未计入时,"缓存节省了多少"这个数字是虚高的。
1.4 缓存命中的 token 单价不同
Provider 的 prompt caching 会在 usage 中区分"缓存命中的输入 token"与"新计算的输入 token",两者单价不同。
# ❌ 只看总数,按标准单价计算
# 缓存命中率越高 ,高估越严重 —— 也就是说越优化,账算得越不准
cost = usage.input_tokens * PRICE_INPUT + usage.output_tokens * PRICE_OUTPUT
# ✅ 分档计算
# cached 部分通常是标准价的 1/10 左右,具体比例按 provider 文档
cost = (
usage.input_tokens_cached * PRICE_INPUT_CACHED
+ usage.input_tokens_new * PRICE_INPUT
+ usage.output_tokens * PRICE_OUTPUT
)
二、网关侧打标方案
2.1 四个设计要点
标签维度必须一次设计到位。 至少包含团队、项目、环境(prod / staging)、模型、调用来源。事后无法补加维度 —— 历史数据里没有该字段。
分档存储 token 数。
-- ❌ 只存总数,1.4 节的问题无解
input_tokens INTEGER,
output_tokens INTEGER
-- ✅ 分档存储,且保留原始 usage 以备重算
input_tokens_new INTEGER, -- 新计算的输入 token
input_tokens_cached INTEGER, -- 命中 provider 缓存的输入 token
output_tokens INTEGER,
raw_usage JSONB -- provider 原始响应,用 于口径变更后重算历史
单价表需带生效时间。
-- provider 调价后,历史账单必须按当时价格计算。
-- 单价写死在代码里,则调价一次全部历史报表失真。
CREATE TABLE model_pricing (
model TEXT,
price_type TEXT, -- input_new | input_cached | output
unit_price NUMERIC,
effective_from TIMESTAMPTZ,
effective_to TIMESTAMPTZ -- NULL 表示当前生效
);
成本计算放在异步链路。 Agent 网关 · 性能与形态代价中 LiteLLM 的 Rust 网关即采用此方式:会话结束后异步记账,不占用请求热路径。记账延迟用户无感,请求延迟用户有感。
三、平台选型
| 项目 | ★ | 协议 | 定位 |
|---|---|---|---|
| Langfuse | 33,369 | NOASSERTION | 功能最全,部署最重 |
| Arize Phoenix | 11,105 | NOASSERTION | 单进程起步,评测原生 |
| OpenLLMetry | 7,384 | Apache-2.0 | 只做埋点,不含后端 |